package asteroids;

import java.util.*;

import be.kuleuven.cs.som.annotate.Basic;
import be.kuleuven.cs.som.annotate.Immutable;

/**
 * @invar	| isValidDimension(width)
 * @invar	| isValidDimension(height)
 * @invar	| hasProperSpaceEntities()
 * 
 * @author 	Antoine Lammens & Yannick Laevaert
 * @version 2.0
 *
 */
public class World {

	/**
	 * Variable registering the width of the world expressed in kilometres.
	 */
	private final double width;
	
	/**
	 * Variable registering the height of the world expressed in kilometres.
	 */
	private final double height;
	
	/**
	 * Constant reflecting the highest possible value for the dimension of a world.
	 */
	private static final double DIMENSION_UPPER_BOUND = Double.MAX_VALUE;
	
	/**
	 * Variable reflecting a collection of all space entities in the world.
	 */
	private Set<SpaceEntity> spaceEntities;
	
	/**
	 * Initialize this new world with default width and height.
	 * 
	 * @effect	| this(0, 0)
	 */
	public World() {
		this(0.0, 0.0);
	}
	
	/**
	 * Initialize this new world with given width and height.
	 * 
	 * @param 	width
	 * 			The width dimension of this new world.
	 * @param 	height
	 * 			The height dimension of this new world.
	 * @pre		The new width dimension must be a valid dimension.
	 * 			| isValidDimension(width)
	 * @pre		The new height dimension must be a valid dimension.
	 * 			| isValidDimension(height)
	 * @post	The new width dimension is equal to the given value.
	 * 			| new.getWidth() == width
	 * @post	The new height dimension is equal to the given value.
	 * 			| new.getHeight() == height
	 */
	public World(double width, double height) {
		assert (isValidDimension(width) && isValidDimension(height)); 
		this.width = width;
		this.height = height;
		spaceEntities = new HashSet<SpaceEntity>();
	}
	
	/**
	 * Check whether the given dimension is a valid dimension.
	 * 
	 * @param 	dimension
	 * 			The dimension to check.
	 * @return 	| result == dimension >= 0 && dimension <= DIMENSION_UPPER_BOUND
	 */
	public boolean isValidDimension(Double dimension) {
		if(!dimension.isNaN())
			return Util.fuzzyGreaterThanOrEqualTo(dimension, 0) && Util.fuzzyLessThanOrEqualTo(dimension, DIMENSION_UPPER_BOUND);
		else
			return false;
	}
	
	
	/**
	 * Add a new space entity to this world.
	 * 
	 * @param 	spaceEntity
	 * 			The space entity which will be added to this world.
	 * @post	| if(doesNotOverlapCurrentEntities(spaceEntity) && liesFullyWithinBounds(spaceEntity)) 
	 * 			|	then (new this).hasAsSpaceEntity(spaceEntity) == true &&
	 *			| 		 (new spaceEntity).getWorld() == this
	 * @post	| if(!doesNotOverlapCurrentEntities(spaceEntity) && liesFullyWithinBounds(spaceEntity) && spaceEntity.getClass() == Bullet.class) 
	 * 			|	then resolveCollision(spaceEntity, searchOverlappingEntity(spaceEntity), null)
	 * @post	| if(! liesFullyWithinBounds(spaceEntity) && spaceEntity.getClass() == Bullet.class)
	 * 			|	then ((Bullet) spaceEntity).getSource().removeBullet((Bullet) spaceEntity)
	 * @throws	IllegalArgumentException
	 * 			| !canHaveAsSpaceEntity(spaceEntity) || (! liesFullyWithinBounds(spaceEntity) && spaceEntity.getClass() != Bullet.class)
	 */
	public void addSpaceEntity(SpaceEntity spaceEntity) throws IllegalArgumentException {
		if(canHaveAsSpaceEntity(spaceEntity)) {
			if(liesFullyWithinBounds(spaceEntity)) {
				spaceEntities.add(spaceEntity);
				spaceEntity.setWorld(this);
				if(!(this.doesNotOverlapCurrentEntities(spaceEntity)) && spaceEntity.getClass() == Bullet.class){
					this.resolveCollision(spaceEntity, this.searchOverlappingEntity(spaceEntity), null);
				}
			}
			else if(spaceEntity.getClass() == Bullet.class)
				((Bullet) spaceEntity).getSource().removeBullet((Bullet) spaceEntity);
			else
				throw new IllegalArgumentException();
		}
		else 
			throw new IllegalArgumentException();
	}			
	
	/**
	 * Remove a space entity from this world.
	 * 
	 * @param 	spaceEntity
	 * 			The space entity which will be removed from this world.
	 * @post	| (new this).hasAsSpaceEntity(spaceEntity) == false
	 * @throws 	IllegalArgumentException
	 * 			| !hasAsSpaceEntity(spaceEntity)
	 */
	public void removeSpaceEntity(SpaceEntity spaceEntity) throws IllegalArgumentException {
		if(hasAsSpaceEntity(spaceEntity)) 
			spaceEntities.remove(spaceEntity);
		else 
			throw new IllegalArgumentException();
	}
	
	/**
	 * Return the width of this world.
	 */
	@Basic @Immutable
	public double getWidth() {
		return width;
	}
	
	/**
	 * Return the height of this world.
	 */
	@Basic @Immutable
	public double getHeight() {
		return height;
	}
	
	/**
	 * Return a set with all the space entities present in this world.
	 */
	@Basic
	public Set<SpaceEntity> getSpaceEntities() {
		return spaceEntities;
	}
	
	/**
	 * Check whether the world can include the given space entity. 
	 * 
	 * @param 	spaceEntity
	 * 			The space entity to check.
	 * @return	True if the given space entity is a valid space entity and the space entity is not located in a world.
	 * 			| result = (spaceEntity != null) && spaceEntity.isValidWorld(this) && !this.hasAsSpaceEntity(spaceEntity) && 
				|			!spaceEntity.isTerminated()
	 */
	public boolean canHaveAsSpaceEntity(SpaceEntity spaceEntity) {
		return (spaceEntity != null) && spaceEntity.isValidWorld(this) && !this.hasAsSpaceEntity(spaceEntity) && 
				!spaceEntity.isTerminated();
	}
	
	/**
	 * Check if the space entities present in this world are all valid space entities.
	 * 
	 * @return	True if all the space entities of this world are valid space entities and lie fully in this world.
	 * 			| for each spaceEntity in getSpaceEntities():
	 * 			| 	result = result && (spaceEntity != null) && spaceEntity.isValidWorld(this) && 
	 *			|				!spaceEntity.isTerminated() && liesFullyWithinBounds(spaceEntity)
	 */
	public boolean hasProperSpaceEntities() {
		boolean result = true;
		for(SpaceEntity spaceEntity: getSpaceEntities()) {
			result = result && (spaceEntity != null) && spaceEntity.isValidWorld(this) && 
					!spaceEntity.isTerminated() && liesFullyWithinBounds(spaceEntity);
		}
		return result;
	}
	
	/**
	 * Check whether the world contains the given space entity. 
	 * 
	 * @param 	spaceEntity
	 * 			The space entity to check.
	 * @return	True if the space entity is present in the set of space entities of this world.
	 * 			| result == getSpaceEntities().contains(spaceEntity)
	 */
	public boolean hasAsSpaceEntity(SpaceEntity spaceEntity) {
		return getSpaceEntities().contains(spaceEntity);
	}
	
	/**
	 * Check whether the given space entity lies fully in this world.
	 * 
	 * @param 	spaceEntity
	 * 			The space entity to check.
	 * @return	True if and only if the given space entity lies fully within the bounds of this world.
	 * 			| result == Util.fuzzyGreaterThanOrEqualTo(spaceEntity.getPosition().getX() - spaceEntity.getRadius(), 0.0) &&
				| 			Util.fuzzyGreaterThanOrEqualTo(spaceEntity.getPosition().getY() - spaceEntity.getRadius(), 0.0) &&
				|			Util.fuzzyLessThanOrEqualTo(spaceEntity.getPosition().getX() + spaceEntity.getRadius(), this.getWidth()) &&
				|			Util.fuzzyLessThanOrEqualTo(spaceEntity.getPosition().getY() + spaceEntity.getRadius(), this.getHeight())
	 */
	public boolean liesFullyWithinBounds(SpaceEntity spaceEntity) {
		return Util.fuzzyGreaterThanOrEqualTo(spaceEntity.getPosition().getX() - spaceEntity.getRadius(), 0.0) &&
				Util.fuzzyGreaterThanOrEqualTo(spaceEntity.getPosition().getY() - spaceEntity.getRadius(), 0.0) &&
				Util.fuzzyLessThanOrEqualTo(spaceEntity.getPosition().getX() + spaceEntity.getRadius(), this.getWidth()) &&
				Util.fuzzyLessThanOrEqualTo(spaceEntity.getPosition().getY() + spaceEntity.getRadius(), this.getHeight());
	}
	
	/**
	 * Check whether the given space entity does not overlap with other entities in this world.
	 * 
	 * @param 	spaceEntity
	 * 			The space entity to check.
	 * @return 	True when the given space entity is a bullet and overlaps his source.
	 * 			| for each currentSpaceEntity in getSpaceEntities():
	 * 			| 	if (currentSpaceEntity.isSourceOf(spaceEntity))
	 * 			| 		then result = true
	 * @return 	True when the given space entity overlaps with itself.
	 * 			| for each currentSpaceEntity in getSpaceEntities():
	 * 			|	if (currentSpaceEntity == spaceEntity)
	 * 			|  		then result = true
	 * @return	False if the given space entity overlaps another space entity in this world.
	 * 			| for each currentSpaceEntity in getSpaceEntities():
	 * 			|	if (spaceEntity.overlap(currentSpaceEntity)
	 * 			| 		then result = false
	 * 			|		else result = true
	 */
	public boolean doesNotOverlapCurrentEntities(SpaceEntity spaceEntity) {
		boolean overlap = false;
		for(SpaceEntity currentSpaceEntity: getSpaceEntities()) {
			if(currentSpaceEntity.isSourceOf(spaceEntity) || currentSpaceEntity == spaceEntity) {
				// Do nothing.
			}
			else {
				overlap = spaceEntity.overlap(currentSpaceEntity);
				if(overlap)
					return false;
			}
		}
		return true;
	}
	
	/**
	 * Advance the state of this world for the given seconds.
	 * 
	 * @param 	dt
	 * 			The number of seconds the world will be advanced.
	 * @param 	collisionListener
	 * 			
	 * @effect 	If there would not occur a collision in the given time interval, the space entities located in this world
	 * 			will be advanced for the given seconds.
	 * 			| for each spaceEntity in getSpaceEntities():
	 * 			|	if(spaceEntity.getTimeToBoundaryCollision() > timeToAdvance) && 
	 * 			|	for each interactionSpaceEntity in getSpaceEntities():
	 * 			|		if(spaceEntity.getTimeToCollision(interactionSpaceEntity) > timeToAdvance)
	 * 			|			then advance(timeToAdvance)
	 * @effect 	If there would occur a collision in the given time interval, the time duration to the first collision will be 
	 * 			calculated, the space entities in the world will be advanced for that time duration and the collision will be
	 * 			resolved. Then this method is invoked for the resting time.
	 * 			| for each spaceEntity in getSpaceEntities():
	 * 			| 	if(spaceEntity.getTimeToBoundaryCollision() < timeToAdvance && spaceEntity.getTimeToBoundaryCollision() > 0.0) {
	 *			|		then timeToAdvance = spaceEntity.getTimeToBoundaryCollision();
	 *			| 	for each interactionSpaceEntity in getSpaceEntities():
	 * 			|		if(spaceEntity.getTimeToCollision(interactionSpaceEntity) < timeToAdvance && spaceEntity.getTimeToBoundaryCollision() > 0.0)
	 * 			|			then timeToAdvance = spaceEntity.getTimeToCollision(interactionSpaceEntity)
	 * 			| if (timeToAdvance < dt)
	 * 			| 	then resolveCollision(collisionEntity, interactionEntity, collisionListener) &&
	 * 			| 	     evolve (dt - timeToAdvance)
	 * @note	timeToAdvance is the smallest time for which a collision will happen.
	 * @note	collisionEntity and interactionEntity in the method resolveCollision are those entities for which timeToAdvance 
	 * 			is the smallest. If timeToAdvance is the smallest for a collision with a boundary, then interactionEntity is null.
	 */
	public void evolve(double dt, CollisionListener collisionListener) {
		double timeToAdvance = dt;
		SpaceEntity collisionEntity = null, interactionEntity = null;
		for(SpaceEntity spaceEntity: getSpaceEntities()) {
			if(spaceEntity.getTimeToBoundaryCollision() < timeToAdvance &&
					spaceEntity.getTimeToBoundaryCollision() > 0.0) {
				timeToAdvance = spaceEntity.getTimeToBoundaryCollision();
				collisionEntity = spaceEntity;
				interactionEntity = null;
			}
			for(SpaceEntity interactionSpaceEntity: getSpaceEntities()){
				if(spaceEntity.getTimeToCollision(interactionSpaceEntity) < timeToAdvance && 
						spaceEntity.getTimeToCollision(interactionSpaceEntity) > 0.0) {
					timeToAdvance = spaceEntity.getTimeToCollision(interactionSpaceEntity);
					collisionEntity = spaceEntity;
					interactionEntity = interactionSpaceEntity;
				}
			}
		}
		advance(timeToAdvance);
		if(timeToAdvance < dt){
			resolveCollision(collisionEntity, interactionEntity, collisionListener);
			evolve(dt - timeToAdvance, collisionListener);
		}
	}
		
	/**
	 * Advance the space entities in this world for the given seconds
	 * 
	 * @param	dt
	 * 			The number of seconds the space entities will be advanced.
	 * @effect	| for each spaceEntity in getSpaceEntities():
	 * 			|	spaceEntity.move()
	 * @effect	| for each spaceEntity in getSpaceEntities():
	 * 			| 	if(spaceEntity.getClass() == Ship.class && ((Ship) spaceEntity).getStateOfThruster())
	 *			|		then ((Ship) spaceEntity).thrust(dt)
	 * @effect	| for each ship in shipsWithProgram:
	 * 			|	for each i in 1..dt/0.2:
	 * 			|		ship.getProgram().execute(ship)
	 * @note	shipsWithProgram contains all ships with a program.
	 */
	public void advance(double dt) {
		List<Ship> shipsWithProgram = new ArrayList<Ship>();
		for(SpaceEntity spaceEntity: getSpaceEntities()) {
			spaceEntity.move(dt);
			if(spaceEntity.getClass() == Ship.class && ((Ship) spaceEntity).getStateOfThruster()) {
				((Ship) spaceEntity).thrust(dt);
			}
			if(spaceEntity.getClass() == Ship.class && ((Ship) spaceEntity).hasProgram()) {
				shipsWithProgram.add((Ship) spaceEntity);
			}
		}
		for(Ship ship: shipsWithProgram) {
			for(int i=0; i < dt/0.2; i++) {
				ship.getProgram().executeProgram(ship);
			}
		}
	}
	
	/**
	 * Resolve the collision between a space entity and a boundary, or between two space entities in this world.
	 * 
	 * @param 	collisionEntity
	 * 			A space entity that collides with a second space entity or with a boundary.
	 * @param  	interactionEntity
	 * 			The second space entity that collides with the first space entity.
	 * @param 	collisionListener
	 * 
	 * @effect	If the interaction entity is a null object, the collision entity bounces with the boundary.
	 * 			| if (interactionEntity == null)
	 * 			| 	then collisionEntity.bounce()
	 * @effect	If the collision entity or the interaction entity is an object of the class bullet and the interaction entity
	 * 			is not a null object, both entities are terminated.
	 * 			| if ((collisionEntity.getClass() == Bullet.class || interactionEntity.getClass() == Bullet.class) && 
	 * 			|		interactionEntity != null)
	 * 			|	then collisionEntity.terminate() &&
	 *			|		 interactionEntity.terminate()
	 * @effect	If the collision entity and the interaction entity are both objects of the class ship, or both objects of 
	 * 			the class asteroid, the collision entity bounces with the interaction entity.
	 * 			| if ((collisionEntity.getClass() == Ship.class && interactionEntity.getClass() == Ship.class) ||
	 * 			|		 (collisionEntity.getClass() == Asteroid.class && interactionEntity.getClass() == Asteroid.class))
	 * 			|	then collisionEntity.bounceWith(interactionEntity)
	 * @effect	If collision entity is an asteroid and interaction entity is a ship, or vice-versa, the ship entity is terminated
	 * 			and the asteroid entity remains unchanged.
	 * 			| if((collisionEntity.getClass() == Ship.class && interactionEntity.getClass() == Asteroid.class) ||
	 * 			|		 (collisionEntity.getClass() == Asteroid.class && interactionEntity.getClass() == Ship.class))
	 * 			|	then ship.terminate
	 */
	public void resolveCollision(SpaceEntity collisionEntity, SpaceEntity interactionEntity, CollisionListener collisionListener) {
		if(interactionEntity == null) {
			// Collision with boundary
			collisionEntity.bounce();
		} 
		else if(collisionEntity.getClass() == Bullet.class || interactionEntity.getClass() == Bullet.class) {
			// Collision with Bullet
			collisionEntity.terminate();
			interactionEntity.terminate();
			if(collisionListener != null) {
				collisionListener.objectCollision(collisionEntity, interactionEntity, 
						collisionEntity.getCollisionPosition(interactionEntity).getX(), 
						collisionEntity.getCollisionPosition(interactionEntity).getY());
			}
		}
		else if(collisionEntity.getClass() == interactionEntity.getClass())
			// Collision between 2 ships or between 2 asteroids
			collisionEntity.bounceWith(interactionEntity);
		else {
			// Collision between asteroid and ship
			if(collisionEntity.getClass() == Ship.class)
				collisionEntity.terminate();
			else
				interactionEntity.terminate();
			if(collisionListener != null) {
				collisionListener.objectCollision(collisionEntity, interactionEntity, 
						collisionEntity.getCollisionPosition(interactionEntity).getX(), 
						collisionEntity.getCollisionPosition(interactionEntity).getY());
			}
		}
	}
	
	/**
	 * Return the space entity which overlaps with the given space entity.
	 * 
	 * @param	spaceEntity
	 * 			The space entity to check.
	 * @return	If there aren't any space entities that overlap, the result is null.
	 * 			| for each collisionEntity in getSpaceEntities():
	 * 			|	if (!spaceEntity.overlap(collisionEntity)
	 * 			|		then result = null
	 * @return	If the given space entity overlaps with another space entity, and the given space entity is a bullet and the other is it's
	 * 			source, the result is null.
	 * 			| for each collisionEntity in getSpaceEntities():
	 * 			|	if (spaceEntity.overlap(collisionEntity) && collisionEntity.isSourceOf(spaceEntity))
	 * 			|		then result = null
	 * @return	If the given space entity overlaps with a space entity, but that space entity is the same entity as the given space entity,
	 * 			the result is null.
	 * 			| for each collisionEntity in getSpaceEntities():
	 * 			|	if (spaceEntity.overlap(collisionEntity) && collisionEntity == spaceEntity)
	 * 			|		then result = null
	 * @return	If the given space entity overlaps the other space entity and the other space entity is not a bullet fired by the 
	 * 			given space entity, and the other space entity is not identical to the given space entity,
	 * 			the result is the other space entity.
	 * 			| for each collisionEntity in getSpaceEntities():
	 * 			|	if (spaceEntity.overlap(collisionEntity) && !collisionEntity.isSourceOf(spaceEntity) && collisionEntity != spaceEntity))
	 * 			|		then result = collisionEntity
	 */
	public SpaceEntity searchOverlappingEntity(SpaceEntity spaceEntity) {
		for(SpaceEntity collisionEntity: getSpaceEntities()) {
			if(spaceEntity.overlap(collisionEntity)){
				if(collisionEntity.isSourceOf(spaceEntity) || collisionEntity == spaceEntity) {
					// Do nothing.
				}
				else 
					return collisionEntity;
			}
		}
		return null;
	}
	
	
	
}
